/*
 * Zed Attack Proxy (ZAP) and its related class files.
 *
 * ZAP is an HTTP/HTTPS proxy for assessing web application security.
 *
 * Copyright 2014 The ZAP Development Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.zaproxy.zap.view;

import java.awt.CardLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JToggleButton;
import javax.swing.JToolBar;
import org.apache.log4j.Logger;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.common.AbstractParam;
import org.parosproxy.paros.control.Control;
import org.parosproxy.paros.control.Control.Mode;
import org.parosproxy.paros.extension.AbstractPanel;
import org.parosproxy.paros.model.Model;
import org.parosproxy.paros.model.Session;
import org.parosproxy.paros.view.View;
import org.zaproxy.zap.model.GenericScanner2;
import org.zaproxy.zap.model.ScanController;
import org.zaproxy.zap.utils.DisplayUtils;
import org.zaproxy.zap.utils.SortedComboBoxModel;

/*
 * This is a cleaner version of ScanPanel which doesn't mix functionality and the UI.
 * Implemented as a new set of classes for backwards compatibility with existing add-ons
 */
public abstract class ScanPanel2<GS extends GenericScanner2, SC extends ScanController<GS>>
        extends AbstractPanel {
    private static final long serialVersionUID = 1L;

    protected enum Location {
        start,
        beforeSites,
        beforeButtons,
        beforeProgressBar,
        afterProgressBar
    };

    private final String prefix;

    private final SC controller;
    private JPanel panelCommand = null;
    private JToolBar panelToolbar = null;
    private JLabel scannedCountNameLabel = null;
    private JLabel foundCountNameLabel = null;

    private JComboBox<ScanEntry<GS>> progressSelect = null;
    private SortedComboBoxModel<ScanEntry<GS>> progressModel = new SortedComboBoxModel<>();
    private final ScanEntry<GS> selectScanEntry;

    private JButton stopScanButton = null;
    private ZapToggleButton pauseScanButton = null;
    private JButton newScanButton = null;
    private JButton clearScansButton = null;
    private JButton optionsButton = null;
    private JProgressBar progressBar = null;
    private ScanStatus scanStatus = null;
    private Mode mode = Control.getSingleton().getMode();

    private static Logger log = Logger.getLogger(ScanPanel2.class);

    /**
     * Constructs a {@code ScanPanel2} with the given message resources prefix, tab icon and scan
     * controller.
     *
     * @param prefix the prefix for the resource messages
     * @param icon the icon for the tab of the panel
     * @param controller the scan controller
     * @param scanParam unused
     * @deprecated (2.6.0) Use {@link #ScanPanel2(String, ImageIcon, ScanController)} instead.
     */
    @Deprecated
    public ScanPanel2(String prefix, ImageIcon icon, SC controller, AbstractParam scanParam) {
        this(prefix, icon, controller);
    }

    /**
     * Constructs a {@code ScanPanel2} with the given message resources prefix, tab icon and scan
     * controller.
     *
     * @param prefix the prefix for the resource messages
     * @param icon the icon for the tab of the panel
     * @param controller the scan controller
     * @since 2.6.0
     */
    public ScanPanel2(String prefix, ImageIcon icon, SC controller) {
        super();
        this.prefix = prefix;
        this.controller = controller;
        selectScanEntry =
                new ScanEntry<>(Constant.messages.getString(prefix + ".toolbar.progress.select"));
        initialize(icon);
        log.debug("Constructor " + prefix);
    }

    /** This method initializes this */
    private void initialize(ImageIcon icon) {
        this.setLayout(new CardLayout());
        if (Model.getSingleton().getOptionsParam().getViewParam().getWmUiHandlingOption() == 0) {
            this.setSize(474, 251);
        }
        this.setName(Constant.messages.getString(prefix + ".panel.title"));
        this.setIcon(icon);
        this.add(getPanelCommand(), prefix + ".panel");
        scanStatus = new ScanStatus(icon, Constant.messages.getString(prefix + ".panel.title"));

        if (View.isInitialised()) {
            View.getSingleton()
                    .getMainFrame()
                    .getMainFooterPanel()
                    .addFooterToolbarRightLabel(scanStatus.getCountLabel());
        }
    }

    /**
     * This method initializes panelCommand
     *
     * @return javax.swing.JPanel
     */
    private javax.swing.JPanel getPanelCommand() {
        if (panelCommand == null) {

            panelCommand = new javax.swing.JPanel();
            panelCommand.setLayout(new java.awt.GridBagLayout());
            panelCommand.setName(prefix + ".panel");

            GridBagConstraints gridBagConstraints1 = new GridBagConstraints();
            GridBagConstraints gridBagConstraints2 = new GridBagConstraints();

            gridBagConstraints1.gridx = 0;
            gridBagConstraints1.gridy = 0;
            gridBagConstraints1.insets = new java.awt.Insets(2, 2, 2, 2);
            gridBagConstraints1.anchor = java.awt.GridBagConstraints.NORTHWEST;
            gridBagConstraints1.fill = java.awt.GridBagConstraints.HORIZONTAL;
            gridBagConstraints1.weightx = 1.0D;
            gridBagConstraints2.gridx = 0;
            gridBagConstraints2.gridy = 1;
            gridBagConstraints2.weightx = 1.0;
            gridBagConstraints2.weighty = 1.0;
            gridBagConstraints2.fill = java.awt.GridBagConstraints.BOTH;
            gridBagConstraints2.insets = new java.awt.Insets(0, 0, 0, 0);
            gridBagConstraints2.anchor = java.awt.GridBagConstraints.NORTHWEST;

            panelCommand.add(this.getPanelToolbar(), gridBagConstraints1);
            panelCommand.add(getWorkPanel(), gridBagConstraints2);
        }
        return panelCommand;
    }

    protected GridBagConstraints getGBC(int gridx, int gridy) {
        return this.getGBC(gridx, gridy, 0.0, new Insets(0, 2, 0, 0));
    }

    protected GridBagConstraints getGBC(int gridx, int gridy, double weightx, Insets insets) {
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.gridx = gridx;
        gbc.gridy = gridy;
        gbc.weightx = weightx;
        if (weightx > 0.0) {
            gbc.fill = java.awt.GridBagConstraints.HORIZONTAL;
        }
        gbc.insets = insets;
        gbc.anchor = java.awt.GridBagConstraints.WEST;
        return gbc;
    }

    private javax.swing.JToolBar getPanelToolbar() {
        if (panelToolbar == null) {

            panelToolbar = new javax.swing.JToolBar();
            panelToolbar.setLayout(new GridBagLayout());
            panelToolbar.setEnabled(true);
            panelToolbar.setFloatable(false);
            panelToolbar.setRollover(true);
            panelToolbar.setPreferredSize(new java.awt.Dimension(800, 30));
            panelToolbar.setName(prefix + ".toolbar");

            int x = 0;

            x = this.addToolBarElements(panelToolbar, Location.start, x);

            newScanButton = getNewScanButton();

            if (newScanButton != null) {
                panelToolbar.add(newScanButton, getGBC(x++, 0));
                newScanButton.setEnabled(!Mode.safe.equals(mode));
                panelToolbar.addSeparator();
                x++;
            }

            panelToolbar.add(
                    new JLabel(Constant.messages.getString(prefix + ".toolbar.progress.label")),
                    getGBC(x++, 0));
            panelToolbar.add(getProgressSelect(), getGBC(x++, 0));

            x = this.addToolBarElements(panelToolbar, Location.beforeButtons, x);

            panelToolbar.add(getPauseScanButton(), getGBC(x++, 0));
            panelToolbar.add(getStopScanButton(), getGBC(x++, 0));

            x = this.addToolBarElements(panelToolbar, Location.beforeProgressBar, x);

            panelToolbar.add(getProgressBar(), getGBC(x++, 0, 1.0, new Insets(0, 5, 0, 5)));

            panelToolbar.add(getClearScansButton(), getGBC(x++, 0));

            panelToolbar.add(getActiveScansNameLabel(), getGBC(x++, 0));
            panelToolbar.add(getActiveScansValueLabel(), getGBC(x++, 0));

            x = this.addToolBarElements(panelToolbar, Location.afterProgressBar, x);

            panelToolbar.add(new JLabel(), getGBC(x++, 0, 1.0, new Insets(0, 0, 0, 0))); // Spacer
            if (hasOptionsButton()) {
                panelToolbar.add(getOptionsButton(), getGBC(x++, 0));
            }
        }
        return panelToolbar;
    }

    /**
     * Tells whether or not the tool bar should have a button to open the Options dialogue.
     *
     * <p>Returns {@code true} by default.
     *
     * @return {@code true} if the panel's tool bar should have an options button, {@code false}
     *     otherwise.
     * @since 2.8.0
     * @see #addToolBarElements(JToolBar, Location, int)
     */
    protected boolean hasOptionsButton() {
        return true;
    }

    /**
     * Adds elements to the tool bar. The method is called while initializing the ScanPanel, at the
     * points specified by the {@link Location} enumeration. Should be overridden by all subclasses
     * that want to add new elements to the ScanPanel's tool bar.
     *
     * <p>The tool bar uses a {@code GridBagLayout}, so elements have to be added with a {@code
     * GridBagConstraints}. For this, the {@code getGBC} methods can be used. The {@code gridX}
     * parameter specifies the cell (as used in {@code GridBagConstraints.gridx}) of the current row
     * where the elements can be added.
     *
     * <p>The method must return the new coordinates of the current cell, after the elements have
     * been added.
     *
     * <p><strong>Note:</strong> A button is added at the end of the tool bar to show the Options
     * dialogue, unless {@link #hasOptionsButton()} returns {@code false}. The name of the options
     * panel to show is obtained from the resource message with the key {@code prefix +
     * ".options.title"}.
     *
     * @param toolBar the tool bar
     * @param location the current location where elements will be added
     * @param gridX the x coordinates of the current cell in the {@code GridBagLayout}
     * @return the new coordinates of the current cell, after the elements have been added.
     * @see #getGBC(int, int)
     * @see #getGBC(int, int, double, Insets)
     * @see GridBagConstraints
     * @see GridBagLayout
     */
    protected int addToolBarElements(JToolBar toolBar, Location location, int gridX) {
        return gridX;
    }

    private JLabel getActiveScansNameLabel() {
        if (scannedCountNameLabel == null) {
            scannedCountNameLabel = new javax.swing.JLabel();
            scannedCountNameLabel.setText(
                    Constant.messages.getString(prefix + ".toolbar.ascans.label"));
        }
        return scannedCountNameLabel;
    }

    private JLabel getActiveScansValueLabel() {
        if (foundCountNameLabel == null) {
            foundCountNameLabel = new javax.swing.JLabel();
            foundCountNameLabel.setText(String.valueOf(controller.getActiveScans().size()));
        }
        return foundCountNameLabel;
    }

    private void setActiveScanLabelsEventHandler() {
        List<GS> ascans = controller.getActiveScans();
        getActiveScansValueLabel().setText(String.valueOf(ascans.size()));
        StringBuilder sb = new StringBuilder();
        sb.append("<html>");
        for (GS ascan : ascans) {
            sb.append(ascan.getDisplayName());
            sb.append("<br>");
        }
        sb.append("</html>");

        final String toolTip = sb.toString();
        getActiveScansNameLabel().setToolTipText(toolTip);
        getActiveScansValueLabel().setToolTipText(toolTip);

        scanStatus.setScanCount(ascans.size());
        this.getClearScansButton().setEnabled(controller.getAllScans().size() - ascans.size() > 0);
    }

    protected JProgressBar getProgressBar() {
        if (progressBar == null) {
            progressBar = new JProgressBar(0, 100);
            progressBar.setValue(0);
            progressBar.setSize(new Dimension(80, 20));
            progressBar.setStringPainted(true);
            progressBar.setEnabled(false);
        }
        return progressBar;
    }

    protected JButton getStopScanButton() {
        if (stopScanButton == null) {
            stopScanButton = new JButton();
            stopScanButton.setToolTipText(
                    Constant.messages.getString(prefix + ".toolbar.button.stop"));
            stopScanButton.setIcon(
                    DisplayUtils.getScaledIcon(
                            new ImageIcon(
                                    ScanPanel2.class.getResource("/resource/icon/16/142.png"))));
            stopScanButton.setEnabled(false);
            stopScanButton.addActionListener(
                    new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            GS scanner = getSelectedScanner();
                            if (scanner != null) {
                                controller.stopScan(scanner.getScanId());
                            }
                        }
                    });
        }
        return stopScanButton;
    }

    protected JToggleButton getPauseScanButton() {
        if (pauseScanButton == null) {
            pauseScanButton = new ZapToggleButton();
            pauseScanButton.setToolTipText(
                    Constant.messages.getString(prefix + ".toolbar.button.pause"));
            pauseScanButton.setSelectedToolTipText(
                    Constant.messages.getString(prefix + ".toolbar.button.unpause"));
            pauseScanButton.setIcon(
                    DisplayUtils.getScaledIcon(
                            new ImageIcon(
                                    ScanPanel2.class.getResource("/resource/icon/16/141.png"))));
            pauseScanButton.setRolloverIcon(
                    DisplayUtils.getScaledIcon(
                            new ImageIcon(
                                    ScanPanel2.class.getResource("/resource/icon/16/141.png"))));
            pauseScanButton.setSelectedIcon(
                    DisplayUtils.getScaledIcon(
                            new ImageIcon(
                                    ScanPanel2.class.getResource("/resource/icon/16/131.png"))));
            pauseScanButton.setRolloverSelectedIcon(
                    DisplayUtils.getScaledIcon(
                            new ImageIcon(
                                    ScanPanel2.class.getResource("/resource/icon/16/131.png"))));
            pauseScanButton.setEnabled(false);
            pauseScanButton.addActionListener(
                    new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            GS scanner = getSelectedScanner();
                            if (scanner != null) {
                                if (pauseScanButton.isSelected()) {
                                    controller.pauseScan(scanner.getScanId());
                                } else {
                                    controller.resumeScan(scanner.getScanId());
                                }
                            }
                        }
                    });
        }
        return pauseScanButton;
    }

    private JButton getOptionsButton() {
        if (optionsButton == null) {
            optionsButton = new JButton();
            optionsButton.setToolTipText(
                    Constant.messages.getString(prefix + ".toolbar.button.options"));
            optionsButton.setIcon(
                    DisplayUtils.getScaledIcon(
                            new ImageIcon(
                                    ScanPanel2.class.getResource("/resource/icon/16/041.png"))));
            optionsButton.addActionListener(
                    new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            Control.getSingleton()
                                    .getMenuToolsControl()
                                    .options(
                                            Constant.messages.getString(prefix + ".options.title"));
                        }
                    });
        }
        return optionsButton;
    }

    private JButton getClearScansButton() {
        if (clearScansButton == null) {
            clearScansButton = new JButton();
            clearScansButton.setToolTipText(
                    Constant.messages.getString(prefix + ".toolbar.button.clear"));
            clearScansButton.setIcon(
                    DisplayUtils.getScaledIcon(
                            new ImageIcon(
                                    ScanPanel2.class.getResource(
                                            "/resource/icon/fugue/broom.png"))));
            clearScansButton.setEnabled(false);
            clearScansButton.addActionListener(
                    new ActionListener() {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            clearFinishedScans();
                        }
                    });
        }
        return clearScansButton;
    }

    public void clearFinishedScans() {
        // Remove via controller
        int count = controller.removeFinishedScans();
        if (count > 0) {
            // Some were removed - remove all and add back the remaining ones
            progressModel.removeAllElements();
            progressModel.addElement(selectScanEntry);
            for (GS scan : controller.getAllScans()) {
                progressModel.addElement(new ScanEntry<>(scan));
            }
            updateScannerUI();
        }
        clearScansButton.setEnabled(false);
    }

    public GS getSelectedScanner() {
        Object selectedItem = progressModel.getSelectedItem();
        if (selectedItem == null) {
            return null;
        }

        @SuppressWarnings("unchecked")
        GS scan = ((ScanEntry<GS>) selectedItem).getScan();
        return scan;
    }

    protected JComboBox<ScanEntry<GS>> getProgressSelect() {
        if (progressSelect == null) {
            progressSelect = new JComboBox<>(progressModel);
            progressSelect.addItem(selectScanEntry);
            progressSelect.setSelectedIndex(0);
            progressSelect.setEnabled(false);

            progressSelect.addActionListener(
                    new java.awt.event.ActionListener() {

                        @Override
                        public void actionPerformed(java.awt.event.ActionEvent e) {
                            scannerSelected(getSelectedScanner());
                        }
                    });
        }
        return progressSelect;
    }

    public void updateScannerUI() {
        scannerSelected(this.getSelectedScanner());
    }

    private void scannerSelected(GS scanner) {
        updateProgressAndButtonsState(scanner);
        switchView(scanner);
    }

    private void updateProgressAndButtonsState(GS scanner) {
        if (scanner == null || Mode.safe.equals(Control.getSingleton().getMode())) {
            // Disable everything
            getStopScanButton().setEnabled(false);
            getPauseScanButton().setEnabled(false);
            getPauseScanButton().setSelected(false);
            getProgressBar().setEnabled(false);
        } else if (scanner.isStopped()) {
            getStopScanButton().setEnabled(false);
            getPauseScanButton().setEnabled(false);
            getPauseScanButton().setSelected(false);
            getProgressBar().setEnabled(false);
        } else {
            getStopScanButton().setEnabled(true);
            getPauseScanButton().setEnabled(true);
            getPauseScanButton().setSelected(scanner.isPaused());
            getProgressBar().setEnabled(true);
        }

        if (scanner != null) {
            getProgressBar().setValue(scanner.getProgress());
            getProgressBar().setMaximum(scanner.getMaximum());
        } else {
            getProgressBar().setValue(0);
        }
    }

    public void scanFinshed(final int id, final String host) {
        if (EventQueue.isDispatchThread()) {
            scanFinshedEventHandler(id, host);
        } else {
            try {
                EventQueue.invokeLater(
                        new Runnable() {
                            @Override
                            public void run() {
                                scanFinshedEventHandler(id, host);
                            }
                        });
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }

    private void scanFinshedEventHandler(int id, String host) {
        log.debug("scanFinished " + prefix + " on " + host);
        if (this.getSelectedScanner() != null && this.getSelectedScanner().getScanId() == id) {
            updateProgressAndButtonsState(getSelectedScanner());
        }
        setActiveScanLabelsEventHandler();
    }

    public void scanProgress(
            final int id, final String host, final int progress, final int maximum) {
        if (EventQueue.isDispatchThread()) {
            scanProgressEventHandler(id, host, progress, maximum);
        } else {
            EventQueue.invokeLater(
                    new Runnable() {
                        @Override
                        public void run() {
                            scanProgressEventHandler(id, host, progress, maximum);
                        }
                    });
        }
    }

    private void scanProgressEventHandler(int id, String host, int progress, int maximum) {
        // log.debug("scanProgress " + prefix + " on " + currentSite + " " + progress);
        if (this.getSelectedScanner() != null && id == this.getSelectedScanner().getScanId()) {
            updateProgressAndButtonsState(getSelectedScanner());
        }
        setActiveScanLabelsEventHandler();
    }

    public void scannerStarted(GS scanner) {
        ScanEntry<GS> scanEntry = new ScanEntry<>(scanner);
        this.progressModel.addElement(scanEntry);
        this.getProgressSelect().setEnabled(true);
        this.getProgressSelect().setSelectedItem(scanEntry);
        this.trimProgressList();
        this.scannerSelected(scanner);
    }

    public void trimProgressList() {
        /*
         * We only trim scans that have completed, so if the user kicks off a load of scans then
         * we could have a lot more in the list than the 'maximum'
         */
        if (this.progressModel.getSize() > this.getNumberOfScansToShow() + 1) {
            // Trim past results - the +1 is for the initial 'select scan' message
            for (int i = 1; i < this.progressModel.getSize(); i++) {
                GS scan = this.progressModel.getElementAt(i).getScan();
                if (scan != null && scan.isStopped()) {
                    controller.removeScan(scan.getScanId());
                    this.progressModel.removeElementAt(i);

                    if (this.progressModel.getSize() <= this.getNumberOfScansToShow() + 1) {
                        // Have removed enough
                        break;
                    }
                    // Need to remove more, but the indexes will have changed so go back 1
                    i--;
                }
            }
        }
    }

    public void reset() {
        log.debug("reset " + prefix);

        progressModel.removeAllElements();
        progressSelect.addItem(selectScanEntry);
        progressSelect.setSelectedIndex(0);

        clearScansButton.setEnabled(false);
    }

    public void sessionScopeChanged(Session session) {}

    public void sessionModeChanged(Mode mode) {
        if (newScanButton != null) {
            this.newScanButton.setEnabled(!Mode.safe.equals(mode));
        }
        // This will handle the remaining changes needed
        updateScannerUI();
    }

    protected void unload() {
        if (View.isInitialised()) {
            View.getSingleton()
                    .getMainFrame()
                    .getMainFooterPanel()
                    .removeFooterToolbarRightLabel(scanStatus.getCountLabel());
        }
    }

    protected SC getController() {
        return controller;
    }

    protected abstract Component getWorkPanel();

    protected abstract void switchView(GS scanner);
    /*
     * Returns the scan button. Can return null if not relevant
     */
    protected abstract JButton getNewScanButton();

    protected abstract int getNumberOfScansToShow();

    private static class ScanEntry<GS extends GenericScanner2>
            implements Comparable<ScanEntry<GS>> {

        private final GS scan;
        private final String label;

        public ScanEntry(String label) {
            this.scan = null;
            this.label = label;
        }

        public ScanEntry(GS scan) {
            this.scan = scan;
            this.label = scan.getScanId() + ": " + scan.getDisplayName();
        }

        public GS getScan() {
            return scan;
        }

        @Override
        public int hashCode() {
            return 31 + ((scan == null) ? 0 : scan.getScanId());
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            ScanEntry<?> other = (ScanEntry<?>) obj;
            if (scan == null) {
                return (other.scan == null);
            } else if (other.scan == null) {
                return false;
            }
            return scan.getScanId() == other.scan.getScanId();
        }

        @Override
        public int compareTo(ScanEntry<GS> other) {
            if (other == null || other.scan == null) {
                return 1;
            }
            if (scan == null) {
                return -1;
            }
            return scan.getScanId() - other.scan.getScanId();
        }

        @Override
        public String toString() {
            return label;
        }
    }
}
